//  -------------------------------------------------------------------------
//  Copyright (C) 2020 BMW AG
//  -------------------------------------------------------------------------
//  This Source Code Form is subject to the terms of the Mozilla Public
//  License, v. 2.0. If a copy of the MPL was not distributed with this
//  file, You can obtain one at https://mozilla.org/MPL/2.0/.
//  -------------------------------------------------------------------------

#pragma once

#include "ramses/renderer/RamsesRenderer.h"
#include "ramses/renderer/IRendererEventHandler.h"
#include "ramses/renderer/IRendererSceneControlEventHandler.h"
#include "ramses/client/Scene.h"
#include "impl/RamsesRendererImpl.h"
#include "internal/PlatformAbstraction/PlatformTime.h"
#include <unordered_set>

namespace ramses::internal
{
    class RendererAndSceneTestEventHandler final : public RendererEventHandlerEmpty, public RendererSceneControlEventHandlerEmpty
    {
    public:
        explicit RendererAndSceneTestEventHandler(RamsesRenderer& renderer, std::chrono::milliseconds timeout = std::chrono::milliseconds{ 20000u })
            : m_renderer(renderer)
            , m_timeout(timeout.count() > 0 ? timeout : std::chrono::hours{ 24 })
        {
        }

        void sceneStateChanged(sceneId_t sceneId, RendererSceneState state) override
        {
            m_scenes[sceneId].state = state;
        }

        void sceneFlushed(sceneId_t sceneId, sceneVersionTag_t sceneVersion) override
        {
            m_scenes[sceneId].version = sceneVersion;
        }

        void sceneExpired(sceneId_t sceneId) override
        {
            m_scenes[sceneId].expired = true;
        }

        void sceneRecoveredFromExpiration(sceneId_t sceneId) override
        {
            m_scenes[sceneId].expired = false;
        }

        void displayCreated(displayId_t displayId, ERendererEventResult result) override
        {
            if (result == ERendererEventResult::Ok)
                m_displays.insert(displayId);
        }

        void displayDestroyed(displayId_t displayId, ERendererEventResult result) override
        {
            if (result == ERendererEventResult::Ok)
                m_displays.erase(displayId);
        }

        void offscreenBufferCreated(displayId_t /*displayId*/, displayBufferId_t offscreenBufferId, ERendererEventResult result) override
        {
            if (result == ERendererEventResult::Ok)
                m_offscreenBuffers.insert({ offscreenBufferId, false });
        }

        void offscreenBufferDestroyed(displayId_t /*displayId*/, displayBufferId_t offscreenBufferId, ERendererEventResult result) override
        {
            if (result == ERendererEventResult::Ok)
                m_offscreenBuffers.erase(offscreenBufferId);
        }

        void offscreenBufferLinked(displayBufferId_t offscreenBufferId, sceneId_t /*consumerScene*/, dataConsumerId_t /*consumerId*/, bool success) override
        {
            if (success)
                m_offscreenBuffers[offscreenBufferId] = true;
        }

        void streamAvailabilityChanged(waylandIviSurfaceId_t streamId, bool available) override
        {
            if (available)
            {
                m_availableStreams.insert(streamId);
            }
            else
            {
                m_availableStreams.erase(streamId);
            }
        }

        bool waitForSceneState(ramses::Scene& scene, RendererSceneState state)
        {
            const auto sceneId = scene.getSceneId();
            return waitUntilOrTimeout([&] { return m_scenes.count(sceneId) > 0 && m_scenes.find(sceneId)->second.state == state; }, &scene);
        }

        bool waitForDisplayCreation(displayId_t displayId)
        {
            assert(m_displays.count(displayId) == 0u);
            return waitUntilOrTimeout([&] { return m_displays.count(displayId) > 0; });
        }

        bool waitForDisplayDestruction(displayId_t displayId)
        {
            assert(m_displays.count(displayId) != 0u);
            return waitUntilOrTimeout([&] { return m_displays.count(displayId) == 0; });
        }

        bool waitForOffscreenBufferCreation(displayBufferId_t offscreenBufferId)
        {
            return waitUntilOrTimeout([&] { return m_offscreenBuffers.count(offscreenBufferId) > 0; });
        }

        bool waitForOffscreenBufferDestruction(displayBufferId_t offscreenBufferId)
        {
            return waitUntilOrTimeout([&] { return m_offscreenBuffers.count(offscreenBufferId) == 0; });
        }

        bool waitForOffscreenBufferLink(displayBufferId_t offscreenBufferId)
        {
            return waitUntilOrTimeout([&] { return m_offscreenBuffers.count(offscreenBufferId) > 0 && m_offscreenBuffers.find(offscreenBufferId)->second; });
        }

        bool waitForFlush(sceneId_t sceneId, sceneVersionTag_t sceneVersion)
        {
            return waitUntilOrTimeout([&] { return m_scenes.count(sceneId) > 0 && m_scenes.find(sceneId)->second.version == sceneVersion; });
        }

        bool checkExpiredState(sceneId_t sceneId)
        {
            m_renderer.getSceneControlAPI()->dispatchEvents(*this);
            return m_scenes.count(sceneId) > 0 && m_scenes.find(sceneId)->second.expired;
        }

        void consumePendingEvents()
        {
            m_renderer.dispatchEvents(*this);
            m_renderer.getSceneControlAPI()->dispatchEvents(*this);
        }

        bool waitForStreamSurfaceAvailabilityChange(waylandIviSurfaceId_t streamSource, bool available)
        {
            return waitUntilOrTimeout([&] { return (m_availableStreams.count(streamSource) > 0) == available; });
        }

        void registerAlreadyCreatedDisplay(displayId_t displayId)
        {
            assert(m_displays.count(displayId) == 0u);
            m_displays.insert(displayId);
        }

    private:
        bool waitUntilOrTimeout(const std::function<bool()>& conditionFunction, ramses::Scene* scene = nullptr)
        {
            const std::chrono::steady_clock::time_point timeoutTS = std::chrono::steady_clock::now() + m_timeout;
            while (!conditionFunction() && std::chrono::steady_clock::now() < timeoutTS)
            {
                if (scene)
                    scene->flush(); // flush periodically to not block subscribing in local publish mode

                if (!m_renderer.impl().isThreaded())
                    m_renderer.doOneLoop();

                std::this_thread::sleep_for(std::chrono::milliseconds{ 5 });  // will give the renderer time to process changes

                m_renderer.dispatchEvents(*this);
                m_renderer.getSceneControlAPI()->dispatchEvents(*this);
            }

            return conditionFunction();
        }

        std::unordered_set<displayId_t> m_displays;
        std::unordered_set<waylandIviSurfaceId_t> m_availableStreams;
        std::unordered_map<displayBufferId_t, bool> m_offscreenBuffers; // with linked flag

        struct SceneInfo
        {
            RendererSceneState state = RendererSceneState::Unavailable;
            sceneVersionTag_t version = InvalidSceneVersionTag;
            bool expired = false;
        };
        std::unordered_map<sceneId_t, SceneInfo> m_scenes;

        RamsesRenderer& m_renderer;
        std::chrono::milliseconds m_timeout;
    };
}
